package MusicLandscape.entities;

import MusicLandscape.util.ConsoleScanner;
import MusicLandscape.util.ConsoleScanable;

import java.beans.*;
import java.util.Scanner;

/**
 * represents a piece of music that has been released on some kind of media (CD, vinyl, video, ...)
 *
 * @author Jonas Altrock (ew20b126@technikum-wien.at)
 * @version 5
 * @since ExerciseSheet01
 */
@JavaBean(description = "A piece of music.", defaultProperty = "title")
public class Track implements ConsoleScanable {
    /**
     * the duration of this track in seconds
     * <p>
     * the duration is a non-negative number, duration 0 (zero) represents unknown duration
     */
    private int duration = 0;
    /**
     * the artist who performs this track
     * <p>
     * the performer cannot be null
     */
    private Artist performer = new Artist();

    /**
     * the title of this track.
     */
    private String title;

    /**
     * the artist who wrote this track
     * <p>
     * the writer cannot be null
     */
    private Artist writer = new Artist();

    /**
     * the year in which the Track was or will be produced
     * <p>
     * valid years are between 1900-2999
     */
    private int year = 1900;

    /**
     * creates a default track.
     * <p>
     * a default track has the following values:
     * <ul>
     *  <li>unknown title
     *  <li>duration 0
     *  <li>default writer and performer
     *  <li>year 1900
     * </ul>
     */
    public Track() {
    }

    /**
     * creates a track with a certain title
     * <p>
     * the resulting track has the specified title, all other values are default
     *
     * @param title the title of this track
     */
    public Track(String title) {
        this.title = title;
    }

    /**
     * creates a deep copy of a Track
     *
     * @param other the track to copy
     */
    public Track(Track other) {
        title = other.getTitle();
        duration = other.getDuration();
        performer = new Artist(other.getPerformer());
        writer = new Artist(other.getWriter());
        year = other.getYear();
    }

    /**
     * gets the duration of this track
     *
     * @return the duration
     */
    public int getDuration() {
        return duration;
    }

    /**
     * returns the performer of this track
     *
     * @return the performer
     */
    public Artist getPerformer() {
        return performer;
    }

    /**
     * gets the title of this track. if the title is not known (null) "unknown title" is returned (without quotes)
     *
     * @return the title
     */
    public String getTitle() {
        if (title == null || title.isBlank()) {
            return "unknown title";
        }
        return title;
    }

    /**
     * returns the writer of this track
     *
     * @return the writer
     */
    public Artist getWriter() {
        return writer;
    }

    /**
     * gets the production year of this track
     *
     * @return the year
     */
    public int getYear() {
        return year;
    }

    /**
     * sets the duration
     * <p>
     * a negative value is ignored, the object remains unchanged
     *
     * @param duration the duration to set
     */
    public void setDuration(int duration) {
        if (duration < 0) {
            return;
        }
        this.duration = duration;
    }

    /**
     * sets the performer of this track
     * <p>
     * null arguments are ignored
     *
     * @param performer the performer to set
     */
    public void setPerformer(Artist performer) {
        if (performer == null) {
            return;
        }
        this.performer = performer;
    }

    /**
     * sets the title of this track.
     *
     * @param title the title to set
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * sets the writer of this track
     * <p>
     * null arguments are ignored
     *
     * @param writer the writer to set
     */
    public void setWriter(Artist writer) {
        if (writer == null) {
            return;
        }
        this.writer = writer;
    }

    /**
     * sets the production year of this track
     * <p>
     * valid years are between 1900 and 2999
     * <p>
     * other values are ignored, the object remains unchanged
     *
     * @param year the year to set
     */
    public void setYear(int year) {
        if (year < 1900 || year > 2999) {
            return;
        }
        this.year = year;
    }

    /**
     * this getter is used to check if the writer of this Track is known.
     *
     * @return true if the writer of this track is known (and has a name), false otherwise.
     */
    public boolean writerIsKnown() {
        return isKnown(writer);
    }

    /**
     * Check whether an artist is known.
     *
     * @param artist the artist to check
     * @return whether the artist is known or not
     */
    protected boolean isKnown(Artist artist) {
        return artist != null && artist.getName() != null && !artist.getName().isBlank();
    }

    /**
     * returns a formatted String containing all information of this track.
     * <p>
     * the String representation is (without quotes):
     * <pre>
     * "title by writer performed by performer (min:sec)"
     * </pre>
     * <p>
     * where
     * <ul>
     *   <li>title stands for the title (exactly 10 chars wide) if not set, return unknown
     *   <li>writer stands for the writer name (exactly 10 chars wide, right justified)
     *   <li>performer stands for the performer name (exactly 10 chars wide, right justified)
     *   <li>min is the duration's amount of full minutes (at least two digits, leading zeros)
     *   <li>sec is the duration's remaining amount of seconds (at least two digits, leading zeros)
     * </ul>
     *
     * @return a String representation of this track
     */
    public String getString() {
        String title = ("unknown title").equals(getTitle()) ? "unknown" : getTitle();
        String writer = isKnown(getWriter()) ? getWriter().getName() : "unknown";
        String performer = isKnown(getPerformer()) ? getPerformer().getName() : "unknown";

        // all string parts must be max 10 characters wide
        title = String.format("%-10.10s", title);
        writer = String.format("%10.10s", writer);
        performer = String.format("%10.10s", performer);

        String minutes = String.format("%02d", (duration / 60) % 100);
        String seconds = String.format("%02d", duration % 60);

        return title + " by " + writer + " performed by " + performer + " (" + minutes + ":" + seconds + ")";
    }

    /**
     * returns a String representation of this track
     * <p>
     * the string representation of this track is described
     * in getString()
     *
     * @return the string representation
     */
    public String toString() {
        return getString();
    }

    /**
     * Guides the user through a process that allows scanning/modifying of this track with a text-based user interface.
     * <p>
     * This method allows modification of the following fields, in the order listed:
     *
     * <ul>
     *   <li>title</li>
     *   <li>duration</li>
     *   <li>performer</li>
     *   <li>writer</li>
     *   <li>year</li>
     * </ul>
     *
     * <p>
     * For each modifiable field the process is the following:
     * <ul>
     *   <li>field name and current value are displayed</li>
     *   <li>new value is read and validated</li>
     * </ul>
     * <p>
     * if input is valid, field is set, otherwise a short message is shown and input of this field is repeated.
     * <p>
     * Old values can be kept for all fields by entering an empty string. The operation cannot be cancelled, instead
     * the user must keep all former values by repeatedly entering empty strings.
     *
     * @return whether this object was altered or not
     */
    @Override
    public boolean scan() {
        boolean changed = false;
        System.out.println("You are modifying Track#" + this.hashCode());
        System.out.println("Enter an empty value if you do not want to change a field.");

        // It is important to instantiate Scanner once per scan() call, otherwise the tests fail,
        // as cached Scanner objects might hold onto old System.in mock objects.
        Scanner consoleInput = new Scanner(System.in);

        String newTitle = ConsoleScanner.nonEmptyString.withScanner(consoleInput).scan("Title (" + getTitle() + ")");

        if (newTitle != null) {
            setTitle(newTitle);
            changed = true;
        }

        Integer newDuration = ConsoleScanner.positiveInteger.withScanner(consoleInput).scan("Duration (" + getDuration() + ")");

        if (newDuration != null) {
            setDuration(newDuration);
            changed = true;
        }

        ConsoleScanner<Artist> artistScanner = new ConsoleScanner<>(
                Artist::new,
                (Artist input) -> !input.getName().isBlank(),
                consoleInput
        );

        Artist newPerformer = artistScanner.scan("Performer (" + getPerformer() + ")");

        if (newPerformer != null) {
            setPerformer(newPerformer);
            changed = true;
        }

        Artist newWriter = artistScanner.scan("Writer (" + getWriter() + ")");

        if (newWriter != null) {
            setWriter(newWriter);
            changed = true;
        }

        Integer newYear = ConsoleScanner.positiveInteger.withScanner(consoleInput).scan("Year (" + getYear() + ")");

        if (newYear != null) {
            setYear(newYear);
            changed = true;
        }

        return changed;
    }
}
